/**
 * Copyright(c) by JNet Smartware Group.
 * All rights reserved. Not distribution or redistribution without
 * any permission of JNet Smartware Group.
 * 
 * @author thuc
 * 2010/04/12
 */
package vn.smartware.co.framework.logging;

import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import org.apache.log4j.FileAppender;
import org.apache.log4j.Layout;
import org.apache.log4j.Level;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;

/**
 * DateRollingFileAppender
 * @author thuctt
 *
 */
public class DateRollingFileAppender extends FileAppender {

	/**
	 * The date pattern. By default, the pattern is set to
	 * "'log4j_'yyyy-MM-dd'.log'" meaning daily rollover.
	 */
	private String filePattern = "'log4j_'yyyy-MM-dd'.log'";
	
	/**
	 * max number of dates file is kept
	 */
	private String maxNumberOfDays = "7";
	
	/**
	 * directory store log files
	 */
	private String dir = "/";

	/**
	 * @return the dir
	 */
	public String getDir() {
		return dir;
	}

	/**
	 * @param dir the dir to set
	 */
	public void setDir(String dir) {
		this.dir = dir;
	}

	/**
	 * The next time we estimate a rollover should occur.
	 */
	private volatile long nextCheck = 0;

	private DateFormat fileNameFormat;

	private TimePeriod checkPeriod = TimePeriod.UNDEFINED;

	private long checkSet;

	// The gmtTimeZone is used only in computeCheckPeriod() method.
	static final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");

	/**
	 * The default constructor does nothing.
	 */
	public DateRollingFileAppender() {
	}

	/**
	 * Instantiate a <code>DailyRollingFileAppender</code> and open the file
	 * designated by <code>filename</code>. The opened filename will become the
	 * output destination for this appender.
	 */
	public DateRollingFileAppender(Layout layout, String filename, String fp)
			throws IOException {
		super(layout, filename, true);
		this.filePattern = fp;
		activateOptions();
	}

	/**
	 * The <b>DatePattern</b> takes a string in the same format as expected by
	 * {@link SimpleDateFormat}. This options determines the rollover schedule.
	 */
	public void setFilePattern(String pattern) {
		filePattern = pattern;
	}

	/** Returns the value of the <b>DatePattern</b> option. */
	public String getFilePattern() {
		return filePattern;
	}
	
	/**
	 * Getter
	 * @return String
	 */
	public String getMaxNumberOfDays()
	{
		return maxNumberOfDays;
	}

	/**
	 * Setter
	 * @param maxNumberOfDays
	 */
	public void setMaxNumberOfDays(String maxNumberOfDays)
	{
		this.maxNumberOfDays = maxNumberOfDays;
	}

	/*
	 * This method checks to see if we're exceeding the number of log backups
	 * that we are supposed to keep, and if so, deletes the offending files. It
	 * then delegates to the rollover method to rollover to a new file if
	 * required.
	 */
	protected void cleanup() throws IOException {
		// Check to see if there are already 5 files
		File file = new File(fileName);
		Calendar cal = Calendar.getInstance();
		int maxDays = 7;
		try {
			maxDays = Integer.parseInt(getMaxNumberOfDays());
		} catch (Exception e) {
			// just leave it at 7.
		}
		
		int fmPat = Calendar.DATE;
		if (checkPeriod.equals(TimePeriod.MINUTE)){
			fmPat = Calendar.MINUTE;
		} else if (checkPeriod.equals(TimePeriod.HOUR)){
			fmPat = Calendar.HOUR;
		} else if (checkPeriod.equals(TimePeriod.DAY)){
			fmPat = Calendar.DATE;
		} else if (checkPeriod.equals(TimePeriod.MONTH)){
			fmPat = Calendar.MONTH;
		} 
		
		cal.add(fmPat, -maxDays);
		Date cutoffDate = cal.getTime();
		
		if (file.getParentFile().exists()) {
			File[] files = file.getParentFile().listFiles();
			for (int i = 0; i < files.length; i++) {
				String datePart = null;
				try {
					datePart = files[i].getName();
					Date date = fileNameFormat.parse(datePart);
					if (date.before(cutoffDate)) {
						files[i].delete();
					}
					
				} catch (Exception pe) {
					// This isn't a file we should touch (it isn't named
					// correctly)
				}
			}
		}
	}
	
	public void setFile(String file) {
		throw new RuntimeException("Use setFilePattern instead");
	}

	private synchronized void roll(long t) {
		
		Date now = new Date(t);

		String currentFilename = fileNameFormat.format(now);
		if (!currentFilename.equals(getFile())) {
			computeNextCheckTime(now);
			try {
				//clean old file
				cleanup();
				// this sets file property and switches output
				super.setFile(getDir() + currentFilename, fileAppend, bufferedIO, bufferSize);
			} catch (IOException iox) {
				LogLog.error("Problem rolling output to logfile "
						+ getDir() + currentFilename, iox);

				// disable self by setting threshold to infinity
				this.setThreshold(Level.OFF);
				LogLog.error("Appender " + name + " disabled");
			}
		} else {
			// bump check time to prevent thrashing
			if (nextCheck != Long.MAX_VALUE) {
				if (checkPeriod.equals(TimePeriod.MINUTE)){
					nextCheck += 1000 / 4;
				} else {
					nextCheck += 30 * 1000;
				}
			}
		}
	}

	private void computeNextCheckTime(Date now) {

		boolean checkStale = (nextCheck == 0)
				|| (now.getTime() > (checkSet + 10 * 60 * 60 * 1000));
		if (checkStale || checkPeriod == TimePeriod.MONTH) {
			nextCheck = nextCheckTime(now, checkPeriod);
		} else if (checkPeriod == TimePeriod.UNDEFINED) {
			// fail to something safe
			nextCheck = now.getTime() + 1000 * 60 * 60;

		} else {
			// all other periods are regular so next check time can be computed
			// by arithmetic
			long dur = checkPeriod.getDuration();
			nextCheck = now.getTime() + dur;
		}

		checkSet = now.getTime();
	}

	public synchronized void activateOptions() {

		Date now = new Date();
		if (filePattern != null) {
			fileNameFormat = new SimpleDateFormat(filePattern);
			computeCheckPeriod(now);

			// have to generate & set file name before activating super options.
			String currentFilename = fileNameFormat.format(now);
			super.setFile(getDir() + currentFilename);
			super.activateOptions();
			computeNextCheckTime(now);

			printPeriodicity();

			roll(now.getTime());

			try {
				File f = new File(getFile());
				LogLog.debug("Log will initially go to " + f.getCanonicalPath());

			} catch (IOException iox) {
				// too bad.
			}

		} else {
			LogLog.error("FilePattern option is not set for appender [" + name + "].");
		}
	}

	void printPeriodicity() {
		LogLog.debug("Appender [" + name + "] to be rolled every " + checkPeriod);
	}

	// This method computes the roll over period by looping over the
	// periods, starting with the shortest, and stopping when the r0 is
	// different from from r1, where r0 is the epoch formatted according
	// the datePattern (supplied by the user) and r1 is the
	// epoch+nextMillis(i) formatted according to datePattern. All date
	// formatting is done in GMT and not local format because the test
	// logic is based on comparisons relative to 1970-01-01 00:00:00
	// GMT (the epoch).

	private void computeCheckPeriod(Date now) {
		// this depends on the format characters in SimpleDateFormet.

		// DAY is the safe fallback.
		TimePeriod period = TimePeriod.DAY;

		String patt = getFilePattern();
		// remove cruft between tick marks...
		patt = patt.replaceAll("'[^']*'", "");

		if (patt.contains("s") || patt.contains("S")) {
			// time period too short! fail up to a minute
			LogLog.warn("Time period too short in " + name
					+ ", roundiong up to a minute");
			period = TimePeriod.MINUTE;
		} else if (patt.contains("m")) {
			period = TimePeriod.MINUTE;
		} else if (patt.contains("H") || patt.contains("k")) {
			period = TimePeriod.HOUR;
		} else if (patt.contains("a")) {
			period = TimePeriod.HALF_DAY;
		} else if (patt.matches(".*[EFdD].*")) {
			period = TimePeriod.DAY;
		} else if (patt.matches(".*[Ww].*")) {
			period = TimePeriod.WEEK;
		} else if (patt.contains("M")) {
			period = TimePeriod.MONTH;
		} else {
			period = TimePeriod.UNDEFINED;
		}
		checkPeriod = period;

		nextCheck = nextCheckTime(now, period);
	}

	public long getNextCheckTime() {
		return nextCheck;
	}

	private long nextCheckTime(Date now, TimePeriod period) {
		// compute next roll time -- we may be starting in the middle of a
		// period, so we can't just add
		// the duration to the present time

		long val = now.getTime();

		GregorianCalendar cal = new GregorianCalendar();
		cal.setTime(now);
		
		if (period.equals(TimePeriod.MINUTE)){
			cal.clear(Calendar.SECOND);
			cal.clear(Calendar.MILLISECOND);
			cal.add(Calendar.MINUTE, 1);
			val = cal.getTimeInMillis();
			
		} else if (period.equals(TimePeriod.HOUR)){
			cal.clear(Calendar.SECOND);
			cal.clear(Calendar.MILLISECOND);
			cal.clear(Calendar.MINUTE);
			cal.add(Calendar.HOUR_OF_DAY, 1);
			val = cal.getTimeInMillis();
			
		} else if (period.equals(TimePeriod.HALF_DAY)){
			cal.clear(Calendar.SECOND);
			cal.clear(Calendar.MILLISECOND);
			cal.clear(Calendar.MINUTE);
			// this next preserves AM_PM
			cal.set(Calendar.HOUR, 0);
			val = cal.getTimeInMillis();
			
		} else if (period.equals(TimePeriod.DAY)){
			cal.clear(Calendar.SECOND);
			cal.clear(Calendar.MILLISECOND);
			cal.clear(Calendar.MINUTE);
			cal.clear(Calendar.HOUR);
			cal.add(Calendar.DATE, 1);
			val = cal.getTimeInMillis();
			
		} else if (period.equals(TimePeriod.WEEK)){
			cal.clear(Calendar.SECOND);
			cal.clear(Calendar.MILLISECOND);
			cal.clear(Calendar.MINUTE);
			cal.clear(Calendar.HOUR);
			cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
			cal.add(Calendar.WEEK_OF_YEAR, 1);
			val = cal.getTimeInMillis();
			
		} else if (period.equals(TimePeriod.MONTH)){
			GregorianCalendar nextMonth = new GregorianCalendar(cal
					.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, 1);
			val = nextMonth.getTimeInMillis();
			
		} else {
			val = Long.MAX_VALUE;
		}

//		LogLog.debug(String.format("Calculated check time as %d (%Tc)", new Object[]{new Date(val)}));
		return val;
	}

	/**
	 * This method differentiates DailyRollingFileAppender from its super class.
	 * 
	 * <p>
	 * Before actually logging, this method will check whether it is time to do
	 * a rollover. If it is, it will schedule the next rollover time and then
	 * rollover.
	 * */
	protected void subAppend(LoggingEvent event) {
		long t = System.currentTimeMillis();
		if (t >= nextCheck) {
			roll(t);
		}
		super.subAppend(event);
	}
}
